package program;

import java.io.*;
import java.util.*;

import be.kuleuven.cs.som.annotate.*;

import item.*;
import exceptions.*;
import attribute.*;
import program.commands.*;
import program.conditions.*;
import program.conditions.Wall;

/**
 * A class concerning programs, which can be executed by a robot.
 * 
 * @invar       The main command of each program must be a valid main command.
 *                      | canHaveAsMainCommand(getMainCommand())
 * 
 */
public class Program {

	/**
	 * Initialise this new program with its main command.
	 *      The main command is the first command of the program.
	 * 
	 * @param       mainCommand
	 *                      The main command for this new program.
	 * @pre         The given main command must be a valid main command for a program.
	 *                      | canHaveAsMainCommand(mainCommand)
	 * @post        The main command of this robot is equal to the given main command.
	 *                      | new.getMainCommand() == mainCommand           
	 *                      
	 */
	public Program(Commands mainCommand){
		assert(canHaveAsMainCommand(mainCommand));
		this.mainCommand = mainCommand;
	}

	/**
	 * Variable registering the main command of this program.
	 */
	private final Commands mainCommand;

	/**
	 * Return the main command of this program.
	 */
	@Basic @Raw @Immutable
	public Commands getMainCommand() {
		return mainCommand;
	}

	/**
	 * Check whether the given command is a valid main command for a program.
	 * 
	 * @param       mainCommand
	 *                      The main command to check for whether it is a valid one.
	 * @return      Return true if and only if the given main command is effective;
	 *                      else return false.
	 *                      | result == ( mainCommand != null )
	 */
	@Raw
	public boolean canHaveAsMainCommand(Commands mainCommand) {
		return ( mainCommand != null );
	}

	/**
	 * Load the program stored at a given path.
	 * 
	 * @param       path
	 *                      The path to the file which should be loaded.
	 * @return      Return a new program, which is the parsed version 
	 *                      of the string-based loaded program.
	 *                      | ...
	 * @throws      FileNotFoundException
	 *                      The given path is invalid.
	 *                      | ...
	 * @throws      IOException
	 *                      An error occurred while reading the file.
	 *                      | ...
	 * @throws      ProgramAnalyseError
	 *                      The loaded program doesn't start with a valid main command.
	 *                      (eg. the program starts with a condition instead of a command)
	 *                      | ...
	 */
	public static Program loadFromFile(String path) throws FileNotFoundException, IOException, ProgramAnalyseError {
		// Read every line of the file into a list
		FileReader fileReader = new FileReader(path);
		BufferedReader bufferedReader = new BufferedReader(fileReader);
		List<String> lines = new ArrayList<String>();
		String line = null;
		while ((line = bufferedReader.readLine()) != null) {
			lines.add(line);
		}
		bufferedReader.close();

		// Transform the list of lines to a string of a single line
		String program = "";
		for(String programLine : lines)
			program += programLine.trim();

		// Convert the string-based program to an object-based program
		lineCounter = 1;
		ProgramElement parsedProgram = analyse(program);
		if(! (parsedProgram instanceof Commands) )
			throw new ProgramAnalyseError("The program should begin with a valid command.");
		return new Program( (Commands) analyse(program) );
	}

	private static int lineCounter = 1;

	/**
	 * Analyse a string-based program and convert it into an object-based program.
	 * 
	 * @param	program
	 *               The string based program to analyse.
	 * @throws ProgramAnalyseError 
	 */
	private static ProgramElement analyse(String program) throws ProgramAnalyseError {
		List<String> parsedCommand = splitBracketBlocks(program);

		if(parsedCommand.get(0).equals("while") && parsedCommand.size() == 3) {
			try {
				Conditions condition = (Conditions)analyse(parsedCommand.get(1));
				Commands command = (Commands)analyse(parsedCommand.get(2));
				return new WhileLoop(lineCounter++, condition, command);
			} catch(ClassCastException e) {
				throw new ProgramAnalyseError("The valid syntax for a loop is (while (condition) (command))");
			}
		}else if(parsedCommand.get(0).equals("seq") && parsedCommand.size() > 1) {
			try {
				Commands[] commandArray = new Commands[parsedCommand.size()-1];
				int i = 0;
				for(String command : parsedCommand.subList(1, parsedCommand.size())) {
					commandArray[i] = (Commands)analyse(command);
					i++;
				}       
				return new Sequence(lineCounter++, commandArray);
			} catch (ClassCastException e) {
				throw new ProgramAnalyseError("The valid syntax for a sequence is (seq (command) (command) ...)");
			}
		}else if (parsedCommand.get(0).equals("if") && parsedCommand.size() == 4) {
			try {
				Conditions condition = (Conditions) analyse(parsedCommand.get(1));
				Commands firstCommand = (Commands) analyse(parsedCommand.get(2));
				Commands secondCommand = (Commands) analyse(parsedCommand.get(3));
				return new IfSelection(lineCounter++, condition, firstCommand, secondCommand);
			} catch (ClassCastException e) {
				throw new ProgramAnalyseError("The valid syntax for a selection is (if (condition) (command) (command))");
			}
		}else if (parsedCommand.get(0).equals("move") && parsedCommand.size() == 1) {
			return new Move(lineCounter++);
		}else if (parsedCommand.get(0).equals("shoot") && parsedCommand.size() == 1) {
			return new Shoot(lineCounter++);
		}else if (parsedCommand.get(0).equals("pick-up-and-use") && parsedCommand.size() == 1) {
			return new PickupAndUse(lineCounter++);
		}else if (parsedCommand.get(0).equals("turn clockwise") && parsedCommand.size() == 1) {
			return new Turn(lineCounter++, "clockwise");    
		}else if (parsedCommand.get(0).equals("turn counter-clockwise") && parsedCommand.size() == 1) {
			return new Turn(lineCounter++, "counterClockwise");            
		}else if (parsedCommand.get(0).equals("and") && parsedCommand.size() == 3) {
			try {
				Conditions firstCondition = (Conditions) analyse(parsedCommand.get(1));
				Conditions secondCondition = (Conditions) analyse(parsedCommand.get(2));
				return new And(lineCounter++, firstCondition, secondCondition);
			} catch (ClassCastException e) {
				throw new ProgramAnalyseError("The valid syntax for AND is (and (condition) (condition))");
			}
		}else if (parsedCommand.get(0).equals("or") && parsedCommand.size() == 3) {
			try {
				Conditions firstCondition = (Conditions) analyse(parsedCommand.get(1));
				Conditions secondCondition = (Conditions) analyse(parsedCommand.get(2));
				return new Or(lineCounter++, firstCondition, secondCondition);
			} catch (ClassCastException e) {
				throw new ProgramAnalyseError("The valid syntax for OR is (or (condition) (condition))");
			}
		}else if (parsedCommand.get(0).equals("not") && parsedCommand.size() == 2) {
			try {
				Conditions conditions = (Conditions) analyse(parsedCommand.get(1));
				return new Not(lineCounter++, conditions);
			} catch (ClassCastException e) {
				throw new ProgramAnalyseError("The valid syntax for NOT is (not (condition))");
			}
		}else if (parsedCommand.get(0).equals("at-item") && parsedCommand.size() == 1) {
			return new AtItem(lineCounter++);
		}else if (parsedCommand.get(0).equals("can-hit-robot") && parsedCommand.size() == 1) {
			return new CanHitRobot(lineCounter++);
		}else if (parsedCommand.get(0).matches("energy-at-least \\d+") && parsedCommand.size() == 1) {
			try {
				Energy energy = new Energy(Double.parseDouble(parsedCommand.get(0).split(" ")[1]), EnergyUnit.Wattsecond);
				if(!Energy.isValidEnergyUnit(energy.getEnergyUnit()))
					throw new ProgramAnalyseError("The value associated with energy-at-least is an invalid energy amount");
				return new EnergyAtLeast(lineCounter++, energy);
			}catch(NumberFormatException e) {
				throw new ProgramAnalyseError("The value associated with energy-at-least should be numeric.");
			}
		}else if (parsedCommand.get(0).equals("true") && parsedCommand.size() == 1) {
			return new True(lineCounter++);
		}else if (parsedCommand.get(0).equals("wall") && parsedCommand.size() == 1) {
			return new Wall(lineCounter++);
		}else {
			throw new ProgramAnalyseError("There is an unknown command/condition in your program.");
		}       
	}

	/**
	 * Split a string-based program in its components (command and arguments).
	 * 
	 * @param       str
	 *                      The string-based program which should be split up.
	 * @throws ProgramAnalyseError 
	 */
	private static List<String> splitBracketBlocks(String str) throws ProgramAnalyseError {
		// Remove the outer brackets
		if(! (str.startsWith("(") && str.endsWith(")")) )
			throw new ProgramAnalyseError("The brackets in this program mismatch.");
		String command = str.substring(1, str.length()-1);

		// Filter the parts of the main expression
		List<String> parsedCommand = new ArrayList<String>();
		char[] chr = command.toCharArray();
		int pos = 0;
		// Select the keyword eg. while, seq, shoot,...
		int i = 0;
		while(i < chr.length && chr[i] != '(') i++;
		parsedCommand.add(command.substring(pos, i));
		pos = i;                

		// Select the (eventual) arguments
		int openBr = 0;
		for(int j = pos; j < chr.length; j++) {
			if(chr[j] == '(') {
				openBr++;
			}
			if(chr[j] == ')') {
				openBr--;
				if(openBr == 0) {
					parsedCommand.add(command.substring(pos, j+1));
					pos = j+1;
				}
			}
		}       
		return parsedCommand;
	}

	/**
	 * Save this program to a file at the given location.
	 * 
	 * @param       path
	 *                      The location where the program should be stored.
	 * @effect      This program is saved to a file at the given path.
	 * @throws      IOException  
	 *                      If the named file exists but is a directory rather than a regular file, 
	 *                      does not exist but cannot be created, or cannot be opened for any other reason.
	 *                      | ...
	 */
	public void saveToFile(String path) throws IOException {
		PrintWriter out = new PrintWriter(new FileWriter(path));
		out.print(prettyFormat());
		out.close();
	}

	/**
	 * Return a string-formatted version of this program.
	 * 
	 * @return      Return a pretty formatted string, representing the main command
	 *                      of this program.
	 *                      | result == getMainCommand().prettyFormat(0)
	 */
	public String prettyFormat() {
		return getMainCommand().prettyFormat(0);
	}

	/**
	 * Return the next basic command in this program for a robot, 
	 * given the line number of the previous executed command.
	 * 
	 * @param       lineNb
	 *                      The line number of the last executed basic command, 
	 *                      this should be zero (or negative) when no previous command was executed yet.
	 * @param       robot
	 *                      Search the next basic command for this robot.
	 * @return      Return the next basic command of this robot, starting from the
	 *                      line number of the previous command and taking into account 
	 *                      the current state of the robot.
	 *                      | result == getMainCommand().getNextBasicCommand(lineNb, robot)
	 * @throws      EndOfProgramException
	 *                      The program is finished.
	 *                      | getMainCommand().getNextBasicCommand(lineNb, robot) == null
	 */
	public BasicCommand getNextBasicCommand(int lineNb, Robot robot) throws EndOfProgramException{
		BasicCommand next = getMainCommand().getNextBasicCommand(lineNb, robot);
		if(next == null)	throw new EndOfProgramException();
		return next;
	}

}